/******************************************************************************

	COPYRIGHT(C) JONAS 'SORTIE' TERMANSEN 2010.

	This file is part of the Maxsi Library.

	Maxsi Library is free software: you can redistribute it and/or modify it
	under the terms of the GNU Lesser General Public License as published by
	the Free Software Foundation, either version 3 of the License, or (at your
	option) any later version.

	Maxsi Library is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
	or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
	License for more details.

	You should have received a copy of the GNU Lesser General Public License
	along with Maxsi Library. If not, see <http://www.gnu.org/licenses/>.

	Maxsi Library
	A powerful Cross-Platform C++ General Purpose Library that allows you to
	efficiently create high-performance and reliable applications.

	MaxsiTLS.cpp
	A buffer streamer on a TLS (the successor to SSL) connection, which is a
	secure encrypted reliable transport protocol layer.

******************************************************************************/

#include "MaxsiLibrary.h"

BeginMaxsiNamespace

#ifdef Maxsi_Support_GNUTLS
//=============================================================================
//	MaxsiGNUTLS.
//=============================================================================
MAXSI_DEFINE_IMPLEMENTATION(MaxsiGNUTLS, ITLS, MAXSI_INTERFACE_STATIC);

MaxsiGNUTLS::MaxsiGNUTLS()
{
	// This might not be efficient, as this is initialized on run time in all programs
	// using the Maxsi Library, regardless of whether they actually use TLS!
	gnutls_global_init();
}

MaxsiGNUTLS::~MaxsiGNUTLS()
{
	gnutls_global_deinit();
}

IBufferStreamer* MaxsiGNUTLS::Connect(MESTR* HostName, short Port, IBufferStreamer* Proxy)
{
	// GnuTLS requires a reliable transport layer, such as TCP.
	if ( Proxy == NULL ) { SetError(MAXSI_ERROR_INVALID_INPUT); return NULL; }

	// Create an instance of our GnuTLS streamer class.
	MaxsiGNUTLSStreamer* Streamer = (MaxsiGNUTLSStreamer*) CreateInterface("ITLSStreamer");

	// Check if it was actually created.
	if ( Streamer == NULL ) { return NULL; }

	// Set the proxy information on our streamer. This handles all the initialization of
	// the TLS connection, so that it is ready to be used when we return from this function.
	MaxsiError Error = Streamer->SetProxyInformation(Proxy, HostName, Port);

	// Check if the connection is usable.
	if ( Error != MAXSI_ERROR_SUCCESS )	{ SetError(Error); DeleteInterface(Streamer); return NULL; }

	return Streamer;
}

void MaxsiGNUTLS::CloseConnection(IBufferStreamer* Connection)
{
	MaxsiGNUTLSStreamer* Streamer = (MaxsiGNUTLSStreamer*)Connection;

	// The destructor of our streamer does the actual closing.
	DeleteInterface(Streamer);
}


//=============================================================================
//	MaxsiGNUTLSStreamer.
//=============================================================================
MAXSI_DEFINE_IMPLEMENTATION(MaxsiGNUTLSStreamer, ITLSStreamer, MAXSI_INTERFACE_DYNAMIC);

ssize_t MaxsiGNUTLS__SendData(gnutls_transport_ptr_t UserData, void* Buffer, size_t BufferSize)
{
	IBufferStreamer*	Proxy	=	(IBufferStreamer*)UserData;

	MaxsiError			Result 	=  Proxy->Write((BYTE*)Buffer, BufferSize);

	if ( Result != MAXSI_ERROR_SUCCESS ) { return -1; }

	MAXSI_TODO("We might have to set errno properly here!");

	return (ssize_t)BufferSize;
}

ssize_t MaxsiGNUTLS__ReadData(gnutls_transport_ptr_t UserData, const void* Buffer, size_t BufferSize)
{
	size_t ActualRead = 0;

	IBufferStreamer*	Proxy	=	(IBufferStreamer*)UserData;

	MaxsiError			Result 	=	Proxy->Read((BYTE*)Buffer, BufferSize, &ActualRead);

	if ( Result != MAXSI_ERROR_SUCCESS ) { return -1; }

	MAXSI_TODO("We might have to set errno properly here!");

	return ActualRead;
}

MaxsiGNUTLSStreamer::MaxsiGNUTLSStreamer()
{
	SessionInitialized					=	false;
	CertificateCredentialsInitialized	=	false;	
}

MaxsiGNUTLSStreamer::~MaxsiGNUTLSStreamer()
{
	if ( SessionInitialized ) { gnutls_bye(Session, GNUTLS_SHUT_RDWR); gnutls_deinit(Session); SessionInitialized = false; }
	if ( CertificateCredentialsInitialized ) { gnutls_certificate_free_credentials(CertificateCredentials); }	
}

MaxsiError MaxsiGNUTLSStreamer::SetProxyInformation(IBufferStreamer* NewProxy, MESTR* HostName, short Port)
{
	if ( NewProxy == NULL || HostName == NULL ) { return MAXSI_ERROR_INVALID_INPUT; }

	Proxy = NewProxy;

	// X509 stuff.
	if ( gnutls_certificate_allocate_credentials(&CertificateCredentials) != 0 ) { return MAXSI_ERROR_UNSPECIFIED; }
	CertificateCredentialsInitialized = true;

	MAXSI_TODO("We need a file containing the trusted CAs!");
	// Sets the trusted CAs file
	//gnutls_certificate_set_x509_trust_file(xcred, CAFILE, GNUTLS_X509_FMT_PEM);

	if ( gnutls_init(&Session, GNUTLS_CLIENT) != 0 ) { return MAXSI_ERROR_UNSPECIFIED; }
	SessionInitialized = true;

	// Use default priorities.
	const char* ErrorDescription = NULL;
	int Status = gnutls_priority_set_direct(Session, "PERFORMANCE", &ErrorDescription);

	if ( Status == GNUTLS_E_INVALID_REQUEST ) { return MAXSI_ERROR_INVALID_INPUT; }
	if ( Status < 0 ) { return MAXSI_ERROR_UNSPECIFIED; }

	// Put the x509 credentials to the current session.
	if ( gnutls_credentials_set(Session, GNUTLS_CRD_CERTIFICATE, CertificateCredentials) != 0 ) { return MAXSI_ERROR_UNSPECIFIED; }

	// Now let's set the callback functions to our transport protocol.
	gnutls_transport_set_pull_function(Session, MaxsiGNUTLS__SendData);
	gnutls_transport_set_push_function(Session, MaxsiGNUTLS__ReadData);
	gnutls_transport_set_ptr(Session, (gnutls_transport_ptr_t)NewProxy);

	// Do a handshake before we continue our initialization.
	if ( gnutls_handshake(Session) < 0 ) { return MAXSI_ERROR_TLS_HANDSHAKE_FAILURE; }

	return MAXSI_ERROR_SUCCESS;	
}

int MaxsiGNUTLSStreamer::GetFlags() { return Proxy->GetFlags(); }
FileSize_t MaxsiGNUTLSStreamer::GetSize() { return Proxy->GetSize(); }
MaxsiError MaxsiGNUTLSStreamer::SetSize(FileSize_t NewSize) { return Proxy->SetSize(NewSize); }
FileSize_t MaxsiGNUTLSStreamer::GetPosition() { return Proxy->GetPosition(); }
MaxsiError MaxsiGNUTLSStreamer::Seek(FileSize_t Offset) { return Proxy->Seek(Offset); }

MaxsiError MaxsiGNUTLSStreamer::Read(BYTE* Destination, size_t DestinationSize, size_t* ActualRead)
{
	size_t		Left	=	DestinationSize;

	while ( Left )
	{
		ssize_t		Read	=	gnutls_record_recv(Session, Destination+(DestinationSize-Left), Left);

		if ( Read == -1 ) { return MAXSI_ERROR_UNSPECIFIED; }

		Left	-=	(size_t)Read;
	}

	return DestinationSize;
}

MaxsiError MaxsiGNUTLSStreamer::ReadFrom(FileSize_t Offset, BYTE* Destination, size_t DestinationSize, size_t* ActualRead) { return Proxy->ReadFrom(Offset, Destination, DestinationSize, ActualRead); }

MaxsiError MaxsiGNUTLSStreamer::Write(BYTE* Source, size_t SourceSize)
{
	size_t		Left	=	SourceSize;

	while ( Left )
	{
		ssize_t		Written		=	gnutls_record_send(Session, Source+(SourceSize-Left), Left);

		if ( Written == -1 ) { return MAXSI_ERROR_UNSPECIFIED; }

		Left	-=	(size_t)Written;
	}

	return SourceSize;
}

MaxsiError MaxsiGNUTLSStreamer::WriteTo(FileSize_t Offset, BYTE* Source, size_t SourceSize) { return Proxy->WriteTo(Offset, Source, SourceSize); }

MaxsiError MaxsiGNUTLSStreamer::WriteFlush()
{
	MAXSI_TODO("How do we flush output using GnuTLS?");
	return MAXSI_ERROR_NOT_IMPLEMENTED;
}


void MaxsiGNUTLSStreamer::SetConnectionHandle(MaxsiHandle Handle)
{
	// We don't use this. Do nothing.
}

MaxsiHandle MaxsiGNUTLSStreamer::GetConnectionHandle()
{
	// This isn't used, just return NULL.
	return NULL;
}

#endif

EndMaxsiNamespace

